if判斷式是每個人第一次學寫程式時會學到的語法,主要用於根據條件來控制流程。雖然if判斷式每個人都會寫,但是如何使用if判斷式,讓主要流程與次要流程在閱讀時有脈絡可循,就是一個值得思考的方向。
在DeadAnts的例子中,Count(string ants)檢查了輸入是否符合預期,確認符合預期後計算dead ants數量,不符合的話就回傳0。
public class DeadAnts
{
public int Count(string ants)
{
if (!string.IsNullOrEmpty(ants))
{
return CalculateDeadAntCount(FindDeadAnts(ants));
}
else
{
return 0;
}
}
private string FindDeadAnts(string ants)
{
return new Regex("ant|[^ant]").Replace(ants, "");
}
private int CalculateDeadAntCount(string deadAntsCollections)
{
if (!string.IsNullOrEmpty(deadAntsCollections))
{
return deadAntsCollections.GroupBy(x => x).Max(x => x.Count());
}
else
{
return 0;
}
}
}
我們從方法名稱可以知道,計算dead ants才是這個方法的主要目的。所以把if判斷是稍微調整一下,讓參數不符合預期時可以在一開始就回傳,這種if判斷是稱為guard if。這樣的好處是可以主要流程在縮排的第一層,比較能凸顯重要的流程。
public int Count(string ants)
{
if (string.IsNullOrEmpty(ants))
{
return 0;
}
return CalculateDeadAntCount(FindDeadAnts(ants));
}
guard if常用在提前結束非正常流程,像是檢查參數是否符合需求就十分適合使用guard if提前結束流程。假設有一個新的需求,dead ants長度小於10時回傳1,此時就可以透過guard if來判斷,讓主要流程維持在第一層縮排。
public int Count(string ants)
{
if (string.IsNullOrEmpty(ants))
{
return 0;
}
var deadAnts = FindDeadAnts(ants);
if (deadAnts.Length < 10)
{
return 1;
}
return CalculateDeadAntCount(FindDeadAnts(deadAnts));
}
在Potter的例子中,可以發現在計算折扣時,使用了一長串的if判斷式。因為每個一個折扣的計算方式都是等同重要的,所以不適合使用guard if來簡化。
public decimal BuyHarryPotter(List<int> numbersOfVolumes)
{
var maxCombination = numbersOfVolumes.Count(numberOfEachVolume => numberOfEachVolume > 0);
return GetCombinationPrice(maxCombination) + BuyHarryPotter(GetRemainingBooks(numbersOfVolumes));
}
private decimal GetCombinationPrice(int maxCombination)
{
return maxCombination * 100 * GetDiscount(maxCombination);
}
private List<int> GetRemainingBooks(List<int> eachBookAmount)
{
return eachBookAmount.Select(x => x > 0 ? x - 1 : x).ToList();
}
private decimal GetDiscount(int maxCombination)
{
if (maxCombination == 2)
{
return 0.95m;
}
else if (maxCombination == 3)
{
return 0.9m;
}
else if (maxCombination == 4)
{
return 0.8m;
}
else if (maxCombination == 5)
{
return 0.75m;
}
else
{
return 1;
}
}
這種情況下可以把計算折扣的邏輯放到Dictionary中,提供狀態給Dictionary,Dictionary就能幫我們選擇符合條件的折扣。
private readonly Dictionary<int, decimal> _discountDict = new Dictionary<int, decimal>()
{
{2, 0.95m},
{3, 0.9m},
{4, 0.8m},
{5, 0.75m}
};
public decimal BuyHarryPotter(List<int> eachBookAmount)
{
var maxCombination = eachBookAmount.Count(x => x > 0);
return GetCombinationPrice(maxCombination) + BuyHarryPotter(GetRemainingBooks(eachBookAmount));
}
private decimal GetCombinationPrice(int maxCombination)
{
return (_discountDict.ContainsKey(maxCombination)
? maxCombination * 100 * _discountDict[maxCombination]
: maxCombination * 100);
}
private List<int> GetRemainingBooks(List<int> eachBookAmount)
{
return eachBookAmount.Select(x => x > 0 ? x - 1 : x).ToList();
}
Dictionary的Value可以放各式各樣的物件,在前天提到的Reverse polish notation calculator的例子裡,也是透過Dictionary來選擇不同的運算邏輯。
var operatorDict = new Dictionary<string, Func<double, double, double>>()
{
{"+", (operator2, operator1) => operator1 + operator2 },
{"-", (operator2, operator1) => operator1 - operator2 },
{"*", (operator2, operator1) => operator1 * operator2 },
{"/", (operator2, operator1) => operator1 / operator2 },
};
Dictionary比較適合邏輯比較簡單的情境,假設折扣還需要考慮不同使用者的身份或是否促銷期等相依的話,Dictionary的Value可能就會變得很複雜,在這種情況下,則會比較建議實作Strategy Pattern,關於Strategy Pattern之後若有機會則會在介紹。
不管是guard if或是Dictionary都是為了讓代碼和流程更為簡潔,減輕閱讀代碼的負擔。